import os
import json
import logging
import paramiko
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
import time
# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("sync_logs.log"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger("file_sync")

class SFTPFileSync:
    def __init__(self, config_file="sync_config.json"):
        """初始化文件同步工具，加载配置"""
        self.config_file = config_file
        self.config = self._load_config()
        if not self.config:
            raise ValueError("配置文件加载失败")
    
    def _load_config(self):
        """从JSON文件加载配置"""
        if not os.path.exists(self.config_file):
            logger.error(f"配置文件 {self.config_file} 不存在")
            return None
        
        try:
            with open(self.config_file, 'r', encoding='utf-8') as f:
                config = json.load(f)
            
            # 验证配置格式
            if "tasks" not in config or not isinstance(config["tasks"], list):
                logger.error("配置文件缺少必要字段: tasks 或格式不正确")
                return None
            
            for task in config["tasks"]:
                required_keys = ["name", "source", "destination", "delete_source"]
                for key in required_keys:
                    if key not in task:
                        logger.error(f"任务配置缺少必要字段: {key}")
                        return None
                
                # 验证源和目标配置
                source = task["source"]
                dest = task["destination"]
                
                if source["type"] not in ["local", "sftp"]:
                    logger.error(f"不支持的源类型: {source['type']}")
                    return None
                
                if dest["type"] not in ["local", "sftp"]:
                    logger.error(f"不支持的目标类型: {dest['type']}")
                    return None
                
                if source["type"] == "sftp" and "sftp" not in source:
                    logger.error("源类型为sftp时缺少sftp配置")
                    return None
                
                if dest["type"] == "sftp" and "sftp" not in dest:
                    logger.error("目标类型为sftp时缺少sftp配置")
                    return None
            
            return config
        except Exception as e:
            logger.error(f"加载配置文件出错: {str(e)}")
            return None
    
    def _connect_sftp(self, sftp_config):
        """连接到SFTP服务器并返回SFTP客户端"""
        try:
            client = paramiko.SSHClient()
            client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            
            # 从配置中获取连接参数
            host = sftp_config["host"]
            port = sftp_config.get("port", 22)
            username = sftp_config["username"]
            password = sftp_config.get("password")
            key_path = sftp_config.get("key_path")
            
            # 根据认证方式连接
            if key_path and os.path.exists(key_path):
                private_key = paramiko.RSAKey.from_private_key_file(key_path)
                client.connect(host, port=port, username=username, pkey=private_key)
            else:
                client.connect(host, port=port, username=username, password=password)
            
            logger.info(f"成功连接到SFTP服务器: {host}：{port}")
            return client.open_sftp()
        except Exception as e:
            logger.error(f"连接SFTP服务器失败: {host}：{port}，错误: {str(e)}")
            raise
    
    def _list_files(self, source_type, source_config):
        """列出源文件或目录下的所有文件"""
        try:
            if source_type == "local":
                # 本地目录
                path = source_config["path"]
                if not os.path.exists(path):
                    logger.error(f"本地路径不存在: {path}")
                    return []
                return [os.path.join(path,'/', f) for f in os.listdir(path) 
                        if os.path.isfile(os.path.join(path,'/', f))]
            
            elif source_type == "sftp":
                # SFTP服务器
                sftp = self._connect_sftp(source_config["sftp"])
                path = source_config["path"]
                
                try:
                    # 列出SFTP目录下的文件
                    file_list = []
                    for item in sftp.listdir_attr(path):
                        if item.st_mode & 0o100000:  # S_IFREG: 普通文件
                            file_list.append(os.path.join(path, item.filename))
                    sftp.close()
                    return file_list
                except Exception as e:
                    logger.error(f"列出SFTP文件失败: {path}, 错误: {str(e)}")
                    sftp.close()
                    return []
            
            else:
                logger.error(f"不支持的源类型: {source_type}")
                return []
        except Exception as e:
            logger.error(f"列出文件失败: {str(e)}")
            return []
    
    def _transfer_file(self, source_path, source_type, source_config, dest_path, dest_type, dest_config):
        """传输单个文件，支持四种传输模式"""
        try:
            logger.info(f"开始传输文件: {source_path} -> {dest_path}")
            
            if source_type == "local" and dest_type == "sftp":
                # 本地 → SFTP
                dest_sftp = self._connect_sftp(dest_config["sftp"])
                
                # 确保目标目录存在
                dest_dir = os.path.dirname(dest_path)
                try:
                    dest_sftp.stat(dest_dir)
                except FileNotFoundError:
                    self._create_directory(dest_sftp, dest_dir)
                
                # 上传文件
                dest_sftp.put(source_path, dest_path)
                dest_sftp.close()
            
            elif source_type == "sftp" and dest_type == "local":
                # SFTP → 本地
                source_sftp = self._connect_sftp(source_config["sftp"])
                
                # 确保目标目录存在
                dest_dir = os.path.dirname(dest_path)
                os.makedirs(dest_dir, exist_ok=True)
                
                # 下载文件
                source_sftp.get(source_path, dest_path)
                source_sftp.close()
            
            elif source_type == "sftp" and dest_type == "sftp":
                # SFTP → SFTP (中间临时文件)
                temp_file = f"/tmp/temp_{os.path.basename(source_path)}"
                
                # 从源SFTP下载到临时文件
                source_sftp = self._connect_sftp(source_config["sftp"])
                source_sftp.get(source_path, temp_file)
                source_sftp.close()
                
                # 上传到目标SFTP
                dest_sftp = self._connect_sftp(dest_config["sftp"])
                
                # 确保目标目录存在
                dest_dir = os.path.dirname(dest_path)
                try:
                    dest_sftp.stat(dest_dir)
                except FileNotFoundError:
                    self._create_directory(dest_sftp, dest_dir)
                
                dest_sftp.put(temp_file, dest_path)
                dest_sftp.close()
                
                # 删除临时文件
                os.remove(temp_file)
            
            else:
                logger.error(f"不支持的传输模式: {source_type} → {dest_type}")
                return False
            
            logger.info(f"文件传输成功: {dest_path}")
            return True
        except Exception as e:
            logger.error(f"文件传输失败: {source_path} -> {dest_path}, 错误: {str(e)}")
            return False
    
    def _create_directory(self, sftp, path):
        """在SFTP服务器上递归创建目录"""
        try:
            sftp.stat(path)
        except FileNotFoundError:
            dirname = os.path.dirname(path)
            if dirname != path:  # 避免无限递归
                self._create_directory(sftp, dirname)
            logger.info(f"创建目录: {path}")
            sftp.mkdir(path)
    
    def _delete_file(self, file_path, source_type, source_config):
        """删除源文件"""
        if not source_config.get("delete_source", False):
            logger.info(f"不删除源文件，配置中delete_source为False")
            return
        
        try:
            if source_type == "local":
                # 本地文件
                if os.path.exists(file_path):
                    os.remove(file_path)
                    logger.info(f"删除本地文件: {file_path}")
                else:
                    logger.warning(f"本地文件不存在，无法删除: {file_path}")
            
            elif source_type == "sftp":
                # SFTP文件
                sftp = self._connect_sftp(source_config["sftp"])
                try:
                    sftp.remove(file_path)
                    logger.info(f"删除SFTP文件: {file_path}")
                except Exception as e:
                    logger.error(f"删除SFTP文件失败: {file_path}, 错误: {str(e)}")
                finally:
                    sftp.close()
            
        except Exception as e:
            logger.error(f"删除文件失败: {file_path}, 错误: {str(e)}")
    
    def process_task(self, task):
        """处理单个同步任务"""
        task_name = task["name"]
        logger.info(f"开始处理任务: {task_name}")
        
        source = task["source"]
        dest = task["destination"]
        source_type = source["type"]
        dest_type = dest["type"]
        
        # 列出所有源文件
        file_list = self._list_files(source_type, source)
        if not file_list:
            logger.info(f"任务 {task_name}: 没有找到需要同步的文件")
            return
        
        logger.info(f"任务 {task_name}: 找到 {len(file_list)} 个文件需要同步")
        
        # 处理每个文件
        success_count = 0
        for file_path in file_list:
            # 生成目标路径（保持源文件相对路径）
            if source_type == "local":
                file_path = file_path.replace("\\", "/")
                source_base = source["path"]
                relative_path = os.path.relpath(file_path, source_base)
                dest_path = os.path.join(dest["path"],'/', relative_path)
                
                # 转换路径分隔符（Windows与Unix兼容）
                if dest_type == "sftp":
                    dest_path = dest_path.replace("\\", "/")
                else:
                    dest_path = os.path.normpath(dest_path)
            
            else:  # SFTP源
                source_base = source["path"]
                if file_path.startswith(source_base):
                    relative_path = file_path[len(source_base):].lstrip("/")
                    dest_path = os.path.join(dest["path"], relative_path)
                    
                    # 转换路径分隔符
                    if dest_type == "sftp":
                        dest_path = dest_path.replace("\\", "/")
                    else:
                        dest_path = os.path.normpath(dest_path)
                else:
                    logger.warning(f"文件路径不在源目录下: {file_path}")
                    continue
            
            # 传输文件
            if self._transfer_file(
                file_path, source_type, source, 
                dest_path, dest_type, dest
            ):
                success_count += 1
                
                # 删除源文件
                self._delete_file(file_path, source_type, source)
        
        logger.info(f"任务 {task_name} 完成，成功传输 {success_count}/{len(file_list)} 个文件")
        return success_count, len(file_list)
    
    def sync_files(self, max_workers=1):
        """执行所有同步任务，支持并发"""
        if not self.config or not self.config.get("tasks"):
            logger.info("没有配置任何同步任务")
            return
        
        logger.info(f"开始执行 {len(self.config['tasks'])} 个同步任务")
        
        if max_workers > 1:
            # 并发执行任务
            with ThreadPoolExecutor(max_workers=max_workers) as executor:
                results = list(executor.map(self.process_task, self.config["tasks"]))
        else:
            # 顺序执行任务
            results = []
            for task in self.config["tasks"]:
                results.append(self.process_task(task))
        
        # 汇总结果
        total_success = sum([r[0] for r in results if r])
        total_files = sum([r[1] for r in results if r])
        
        logger.info(f"所有同步任务完成，总共成功传输 {total_success}/{total_files} 个文件")


# 示例配置文件内容 (sync_config.json)
"""
{
  "tasks": [
    {
      "name": "本地到SFTP备份",
      "source": {
        "type": "local",
        "path": "D:/data/files",
        "delete_source": false
      },
      "destination": {
        "type": "sftp",
        "sftp": {
          "host": "sftp.destination.com",
          "port": 22,
          "username": "user",
          "password": "password"
        },
        "path": "/backup/files"
      },
      "delete_source": false
    },
    {
      "name": "SFTP到本地下载",
      "source": {
        "type": "sftp",
        "path": "/remote/data/incoming",
        "sftp": {
          "host": "sftp.source.com",
          "username": "user",
          "password": "password"
        },
        "delete_source": true
      },
      "destination": {
        "type": "local",
        "path": "D:/downloads/processed"
      },
      "delete_source": true
    },
    {
      "name": "SFTP到SFTP同步",
      "source": {
        "type": "sftp",
        "path": "/source/server/files",
        "sftp": {
          "host": "sftp.source.com",
          "username": "user",
          "password": "password"
        },
        "delete_source": false
      },
      "destination": {
        "type": "sftp",
        "sftp": {
          "host": "sftp.destination.com",
          "username": "user",
          "password": "password"
        },
        "path": "/destination/server/files"
      },
      "delete_source": false
    }
  ]
}
"""

def main():
    try:
        sync_tool = SFTPFileSync()
        # 设置并发任务数，默认为1（顺序执行）
        sync_tool.sync_files(max_workers=2)
    except Exception as e:
        logger.critical(f"文件同步过程中发生致命错误: {str(e)}")

if __name__ == "__main__":
    while True:
        main()
        print("等待5秒后继续...")
        time.sleep(5)       